import path from 'node:path'
import { type Config, definePlugin, type Group, getMode } from '@kubb/core'
import { camelCase } from '@kubb/core/transformers'
import type { Oas } from '@kubb/oas'
import { parseFromConfig } from '@kubb/oas'
import { jsonGenerator } from './generators'
import { OperationGenerator } from './OperationGenerator.ts'
import { SchemaGenerator } from './SchemaGenerator.ts'
import type { PluginOas } from './types.ts'

export const pluginOasName = 'plugin-oas' satisfies PluginOas['name']

export const pluginOas = definePlugin<PluginOas>((options) => {
  const {
    output = {
      path: 'schemas',
    },
    group,
    validate = true,
    generators = [jsonGenerator],
    serverIndex,
    contentType,
    oasClass,
    discriminator = 'strict',
  } = options

  const getOas = async ({ config }: { config: Config }): Promise<Oas> => {
    // needs to be in a different variable or the catch here will not work(return of a promise instead)
    const oas = await parseFromConfig(config, oasClass)

    oas.setOptions({
      contentType,
      discriminator,
    })

    try {
      if (validate) {
        await oas.valdiate()
      }
    } catch (e) {
      const error = e as Error

      console.warn(error?.message)
    }

    return oas
  }

  return {
    name: pluginOasName,
    options: {
      output,
      validate,
      discriminator,
      ...options,
    },
    inject() {
      const config = this.config
      let oas: Oas

      return {
        async getOas() {
          if (!oas) {
            oas = await getOas({ config })
          }

          return oas
        },
        async getBaseURL() {
          const oas = await getOas({ config })
          if (serverIndex !== undefined) {
            return oas.api.servers?.at(serverIndex)?.url
          }

          return undefined
        },
      }
    },
    resolvePath(baseName, pathMode, options) {
      const root = path.resolve(this.config.root, this.config.output.path)
      const mode = pathMode ?? getMode(path.resolve(root, output.path))

      if (mode === 'single') {
        /**
         * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
         * Other plugins then need to call addOrAppend instead of just add from the fileManager class
         */
        return path.resolve(root, output.path)
      }

      if (group && (options?.group?.path || options?.group?.tag)) {
        const groupName: Group['name'] = group?.name
          ? group.name
          : (ctx) => {
              if (group?.type === 'path') {
                return `${ctx.group.split('/')[1]}`
              }
              return `${camelCase(ctx.group)}Controller`
            }

        return path.resolve(
          root,
          output.path,
          groupName({
            group: group.type === 'path' ? options.group.path! : options.group.tag!,
          }),
          baseName,
        )
      }

      return path.resolve(root, output.path, baseName)
    },
    async install() {
      if (!output) {
        return
      }

      const oas = await this.getOas()
      await oas.dereference()

      const schemaGenerator = new SchemaGenerator(
        {
          unknownType: 'unknown',
          emptySchemaType: 'unknown',
          dateType: 'date',
          transformers: {},
          ...this.plugin.options,
        },
        {
          fabric: this.fabric,
          oas,
          pluginManager: this.pluginManager,
          plugin: this.plugin,
          contentType,
          include: undefined,
          override: undefined,
          mode: 'split',
          output: output.path,
        },
      )

      const schemaFiles = await schemaGenerator.build(...generators)
      await this.upsertFile(...schemaFiles)

      const operationGenerator = new OperationGenerator(this.plugin.options, {
        fabric: this.fabric,
        oas,
        pluginManager: this.pluginManager,
        plugin: this.plugin,
        contentType,
        exclude: undefined,
        include: undefined,
        override: undefined,
        mode: 'split',
      })

      const operationFiles = await operationGenerator.build(...generators)

      await this.upsertFile(...operationFiles)
    },
  }
})
